package org.koin.test.check;

import org.koin.core.Koin;
import org.koin.core.KoinApplication;
import org.koin.core.annotation.KoinInternalApi;
import org.koin.core.definition.BeanDefinition;
import org.koin.core.error.*;
import org.koin.core.logger.Level;
import org.koin.core.parameter.DefinitionParameters;
import org.koin.core.parameter.ParametersDefinition;
import org.koin.core.qualifier.Qualifier;
import org.koin.core.qualifier.TypeQualifier;
import org.koin.core.scope.Scope;
import org.koin.core.scope.ScopeDefinition;
import org.koin.dsl.KoinAppDeclaration;
import org.koin.mp.KoinPlatformTools;
import org.koin.test.mock.MockProvider;
import org.koin.test.parameter.MockParameter;
import java.util.Iterator;

import static org.koin.dsl.KoinApplication.koinApplication;

public class CheckModules {

    /**
     * Check all definition's dependencies - start all modules and check if definitions can run
     */
    public static void checkModules(KoinApplication koinApplication, CheckParameters parameters) {
        checkModules(koinApplication.getKoin(), parameters);
    }

    public static void checkModules(Koin koin, CheckParameters parametersDefinition) {
        koin.getLogger().info("[Check] checking current modules ...");

        checkScopedDefinitions(koin, declareParameterCreators(koin, parametersDefinition));

        koin.getLogger().info("[Check] modules checked");
        koin.close();
    }

    private static ParameterBinding declareParameterCreators(Koin koin,
                                                             CheckParameters parametersDefinition) {
        ParameterBinding parameterBinding = new ParameterBinding(koin);
        if (parameterBinding != null) {
            if (parametersDefinition != null) {
                parametersDefinition.invoke(parameterBinding);
            }
        }
        return parameterBinding;
    }

    @KoinInternalApi
    private static void checkScopedDefinitions(Koin koin, ParameterBinding allParameters) {
        Iterator<ScopeDefinition> iterator = koin.getScopeRegistry()
                .getScopeDefinitions()
                .values()
                .iterator();
        while (iterator.hasNext()) {
            ScopeDefinition scopeDefinition = iterator.next();
            checkScope(koin, scopeDefinition, allParameters);
        }
    }

    @KoinInternalApi
    private static void checkScope(Koin koin,
                                   ScopeDefinition scopeDefinition,
                                   ParameterBinding allParameters) {
        Qualifier qualifier = scopeDefinition.getQualifier();
        Object sourceScopeValue = mockSourceValue(qualifier);
        try {
            Scope scope = koin.getOrCreateScope(qualifier.value, qualifier, sourceScopeValue);
            Iterator<BeanDefinition> iterator = scope
                    .getScopeDefinition()
                    .getDefinitions()
                    .iterator();
            while (iterator.hasNext()) {
                BeanDefinition beanDefinition = iterator.next();
                scope.getLogger().info("Checking: " + beanDefinition + " ...");
                checkDefinition(allParameters, beanDefinition, scope);
            }
        } catch (ScopeAlreadyCreatedException e) {
            e.printStackTrace();
        } catch (NoScopeDefFoundException e) {
            e.printStackTrace();
        }
    }

    private static <T> T mockSourceValue(Qualifier qualifier) {
        if (qualifier instanceof TypeQualifier) {
            return (T) MockProvider.INSTANCE().makeMock(qualifier.value.getClass());
        }
        return null;
    }

    @KoinInternalApi
    private static void checkDefinition(ParameterBinding allParameters,
                                        BeanDefinition definition,
                                        Scope scope) {
        DefinitionParameters parameters = null;
        ParametersCreator parametersCreator = (ParametersCreator) allParameters
                .getParametersCreators()
                .get(new CheckedComponent(definition.getQualifier(),
                        definition.getPrimaryType()));
        if (parametersCreator != null) {
            parameters = (DefinitionParameters) parametersCreator.invoke(definition.getQualifier());
        }
        if (parameters == null) {
            parameters = null;
        }
        if (parameters == null) {
            parameters = (DefinitionParameters) new MockParameter(scope,
                    allParameters.getDefaultValues());
        }

        Qualifier scopeQualifier = scope.getScopeDefinition().getQualifier();
        if (scopeQualifier instanceof TypeQualifier) {
            scope.setSource(MockProvider.INSTANCE().makeMock(scopeQualifier.getClass()));
        }
        scope.addParameters(parameters);
        try {
            scope.get(definition.getClass(), definition.getQualifier(), new ParametersDefinition() {
                @Override
                public DefinitionParameters invoke() {
                    return null;
                }
            });
            scope.clearParameters();
        } catch (ClosedScopeException e) {
            e.printStackTrace();
        } catch (DefinitionParameterException e) {
            e.printStackTrace();
        } catch (NoBeanDefFoundException e) {
            e.printStackTrace();
        }
    }

    public void checkModules(Level level,
                             CheckParameters parameters,
                             KoinAppDeclaration appDeclaration) {
        try {
            KoinApplication koinApplication = koinApplication(appDeclaration)
                    .logger(KoinPlatformTools.INSTANCE().defaultLogger(level));
            checkModules(koinApplication, parameters);
        } catch (ScopeAlreadyCreatedException e) {
            e.printStackTrace();
        } catch (NoScopeDefFoundException e) {
            e.printStackTrace();
        }
    }

    public void checkModules(Level level,
                             KoinAppDeclaration appDeclaration) {
        checkModules(level, null, appDeclaration);
    }

    public void checkModules(Koin koin) {
        checkModules(koin, null);
    }
}
